package com.nulldev.util.web.HttpServer3.rewrite;

import static com.nulldev.util.web.HttpServer3.util.HttpServerUtil.__get_header;
import static com.nulldev.util.web.HttpServer3.util.HttpServerUtil.__has_header;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.Socket;
import java.time.Duration;
import java.util.Map;

import com.nulldev.util.VariableAPI.ArrayUtils;
import com.nulldev.util.VariableAPI.MathUtil;
import com.nulldev.util.VariableAPI.StringsUtil;
import com.nulldev.util.VariableAPI.util.strings.splitters.Splitters;
import com.nulldev.util.VariableAPI.util.strings.splitters.third_party.Splitter;
import com.nulldev.util.data.Variables;
import com.nulldev.util.data.Arrays.maps.FastMaps;
import com.nulldev.util.io.IOUtils;
import com.nulldev.util.logging.Logger;
import com.nulldev.util.logging.LoggerFactory;
import com.nulldev.util.networking.ip.IP;
import com.nulldev.util.web.HttpServer3.HttpServer;
import com.nulldev.util.web.HttpServer3.ReadRequest;
import com.nulldev.util.web.HttpServer3.Response;
import com.nulldev.util.web.HttpServer3.SSLBehaviour;
import com.nulldev.util.web.HttpServer3.pages.v3.RequestProtocolType;
import com.nulldev.util.web.HttpServer3.util.BoundedInputStream;

class InternalImpl {

	private static final Logger log = LoggerFactory.getLogger();
	private static final int READ_LIMIT = 65536; // 16384;

	static String readTillNextLine(final InputStream is) throws Exception {
		final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(IOUtils.STRING_ALLOC_BUFFER);
		int c;
		while ((c = is.read()) != -1) {
			if (c == ((char) '\r') || c == ((char) '\n')) {
				/* Work around UNIX being funny with \r\n */
				if (c == ((char) '\r'))
					is.read();
				break;
			}
			byteArrayOutputStream.write(c);
			if (byteArrayOutputStream.size() + 1 >= READ_LIMIT) {
				log.dwarn("[readTillNextLine] Hit READ_LIMIT for stream, bailing out.");
				break;
			}
		}
		if (c == -1 && byteArrayOutputStream.size() == 0) {
			return null;
		}
		final String line = byteArrayOutputStream.toString("UTF-8");
		IOUtils.closeQuietly(byteArrayOutputStream);
		return line;
	}

	static int __get_port_http(final SSLBehaviour behaviour, final int defaultPort) {
		switch (Variables.requireNonNullObject(behaviour, "behaviour")) {
			case ATTEMPT_STANDARD:
				return 80;
			case LEGACY_OPTION:
				return -1;
			case ATTEMPT_STANDARD_HTTPS_ONLY:
			default:
				return defaultPort;
		}
	}

	static int __get_port_https(final SSLBehaviour behaviour, final int defaultPort) {
		switch (Variables.requireNonNullObject(behaviour, "behaviour")) {
			case ATTEMPT_STANDARD:
			case ATTEMPT_STANDARD_HTTPS_ONLY:
				return 443;
			case USE_NEAREST_PORT_POSITIVE:
				return IP.freePort(defaultPort + 1, defaultPort + 199);
			case USE_NEAREST_PORT_NEGATIVE:
				return IP.freePort(defaultPort - 199, defaultPort - 1);
			case DISABLED:
				return -1;
			case LEGACY_OPTION:
				return defaultPort;
			default:
				throw new UnsupportedOperationException("Unknown behaviour: " + behaviour);
		}
	}

	static boolean __should_chunk_transfer(final HttpServer3RW hs3, final Response response, final RequestProtocolType protocol) {
		/* Do not chunk if it is generally disabled. */
		if (!hs3.flags().enable_chunked_transfers) {
			return false;
		}
		if (response.shouldPreferChunking()) {
			/* Only respect SHOULD_PREFER_CHUNKING if we're doing a HTTP request. */
			return protocol.isHTTP();
		} else {
			if (!hs3.flags().enable_compressed_chunked_transfers) {
				/* Require enable_compressed_chunked_transfers for compressed chunking. */
				return false;
			} else {
				return !response.copyable() && !response.nioCopyable();
			}
		}
	}

	private static final Splitter TIME_APPLY_TO_SP = Splitter.onPattern("\\, |,");
	static final int ENFORCE_DATA_LIMIT_SIZE = 20 * 1024 * 1024;

	@SuppressWarnings("resource")
	static ReadRequest readRequest(final HttpServer hs3, final Socket s, final InputStream is) throws Exception {
		final Map<String, String> headers = FastMaps.map(15);
		String header;
		while ((header = readTillNextLine(is)) != null) {
			final String[] sp = Splitters.splitString(header, ":");
			if (sp.length == 0)
				break;
			headers.put(sp[0], ArrayUtils.StringArrayUtils.addFrom(sp, 1, ':').substring(1));
		}
		if (headers.containsKey("range"))
			headers.put("Range", headers.remove("range"));
		Duration dur = null;
		if (__has_header(headers, "X-Request-Time-Limit")) {
			try {
				final String tl = __get_header(headers, "X-Request-Time-Limit");
				if (StringsUtil.isNumber(tl)) {
					dur = Duration.ofSeconds((int) StringsUtil.asNumber(tl));
				} else {
					dur = Duration.parse(tl);
				}
			} catch (Exception e) {
				log.error("[readRequest:timeout] Failed to parse duration.", e);
			}
		}
		String[] to = ReadRequest.DEFAULT_APPLY;
		if (__has_header(headers, "X-Request-Time-Apply-To")) {
			try {
				final String tl = __get_header(headers, "X-Request-Time-Apply-To");
				to = TIME_APPLY_TO_SP.splitToArray(tl.toLowerCase());
			} catch (Exception e) {
				log.error("[readRequest:timeout_apply_to] Failed to parse.", e);
			}
		}
		if (__has_header(headers, "Content-Length")) {
			final int cl = MathUtil.safeParseInt(headers.get("Content-Length"), -1);
			log.trace("readRequest() -> Reading " + cl + " bytes of data.");
			final String contentType = headers.get("Content-Type");
			if (hs3.settings().enforceDataSizeLimit && cl > ENFORCE_DATA_LIMIT_SIZE) {
				log.error("readRequest() -> Data uploaded is too large! (enforceDataSizeLimit skips data larger than 20MBs.)");
				return new ReadRequest(headers).setTimeoutDuration(dur).setTimeoutApplyTo(to);
			} else if (cl > 0) {
				return __read_request_post(contentType, headers, is, cl, dur, to);
			} else {
				// No content-length or invalid length...
				return new ReadRequest(headers).setTimeoutDuration(dur).setTimeoutApplyTo(to);
			}
		} else if (__has_header(headers, "Transfer-Encoding")) {
			/// XXX: Implement support for chunked POST.
		}
		return new ReadRequest(headers).setTimeoutDuration(dur).setTimeoutApplyTo(to);
	}

	@SuppressWarnings("resource")
	private static ReadRequest __read_request_post(final String _contentType, final Map<String, String> headers, final InputStream is, final long cl,
			final Duration dur, final String[] to) {
		/* Workaround multipart/form-data's "boundary" */
		final String contentType = _contentType.contains(";") ? _contentType.split(";")[0] : _contentType;
		switch (contentType) {
			case "multipart/form-data":
				/* TODO */
			case "application/x-www-form-urlencoded":
			default:
				return new ReadRequest(headers, new BoundedInputStream(is, cl, false)).setTimeoutDuration(dur).setTimeoutApplyTo(to);
		}
	}

}
